package cn.universal.plugins.protocolapi.core.resource;

import cn.universal.plugins.protocolapi.utils.Assert;
import cn.universal.plugins.protocolapi.utils.IoUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;

/**
 * 数据库资源存储
 *
 * @author mxd
 */
public class DatabaseResource extends KeyValueResource {

  private static final Logger logger = LoggerFactory.getLogger(DatabaseResource.class);
  private final JdbcTemplate template;
  private final String tableName;
  private Map<String, String> cachedContent = new ConcurrentHashMap<>();

  public DatabaseResource(JdbcTemplate template, String tableName) {
    this(template, tableName, false);
  }

  public DatabaseResource(JdbcTemplate template, String tableName, boolean readonly) {
    this(template, tableName, "/magic-api", readonly);
  }

  public DatabaseResource(JdbcTemplate template, String tableName, String path, boolean readonly) {
    this(template, tableName, path, readonly, null);
  }

  public DatabaseResource(
      JdbcTemplate template,
      String tableName,
      String path,
      boolean readonly,
      KeyValueResource parent) {
    super("/", path, readonly, parent);
    this.template = template;
    this.tableName = tableName;
  }

  public DatabaseResource(
      JdbcTemplate template,
      String tableName,
      String path,
      boolean readonly,
      Map<String, String> cachedContent,
      KeyValueResource parent) {
    this(template, tableName, path, readonly, parent);
    this.cachedContent = cachedContent;
  }

  @Override
  public byte[] read() {
    String value = this.cachedContent.get(path);
    if (value == null) {
      String sql = String.format("select file_content from %s where file_path = ?", tableName);
      value = template.queryForObject(sql, String.class, this.path);
      if (value != null) {
        this.cachedContent.put(path, value);
      }
    }
    return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
  }

  @Override
  public void readAll() {
    this.cachedContent.entrySet().removeIf(entry -> entry.getKey().startsWith(path));
    String sql =
        String.format(
            "select file_path, file_content from %s where file_path like '%s%%'",
            tableName, this.path);
    SqlRowSet sqlRowSet = template.queryForRowSet(sql);
    while (sqlRowSet.next()) {
      Object object = sqlRowSet.getObject(2);
      String content = null;
      if (object instanceof String) {
        content = object.toString();
      } else if (object instanceof byte[]) {
        content = new String((byte[]) object, StandardCharsets.UTF_8);
      } else if (object instanceof Blob) {
        Blob blob = (Blob) object;
        try (InputStream is = blob.getBinaryStream()) {
          content = new String(IoUtils.bytes(is), StandardCharsets.UTF_8);
        } catch (SQLException | IOException ex) {
          logger.error("读取content失败", ex);
        }
      } else if (object instanceof Clob) {
        Clob clob = (Clob) object;
        try {
          content = clob.getSubString(1, (int) clob.length());
        } catch (SQLException ex) {
          logger.error("读取content失败", ex);
        }
      }
      Assert.isNotNull(content, "读取content失败，请检查列类型是否正确");
      this.cachedContent.put(sqlRowSet.getString(1), content);
    }
  }

  @Override
  public boolean exists() {
    if (this.cachedContent.get(this.path) != null) {
      return true;
    }
    String sql = String.format("select count(*) from %s where file_path = ?", tableName);
    Long value = template.queryForObject(sql, Long.class, this.path);
    return value != null && value > 0;
  }

  @Override
  public boolean write(String content) {
    String sql = String.format("update %s set file_content = ? where file_path = ?", tableName);
    if (exists()) {
      if (template.update(sql, content, this.path) > 0) {
        this.cachedContent.put(this.path, content);
        return true;
      }
    }
    sql = String.format("insert into %s (file_path,file_content) values(?,?)", tableName);
    if (template.update(sql, this.path, content) > 0) {
      this.cachedContent.put(this.path, content);
      return true;
    }
    return false;
  }

  @Override
  public Set<String> keys() {
    String prefix = isDirectory() ? this.path : (this.path + separator);
    if (!cachedContent.isEmpty()) {
      return cachedContent.keySet().stream()
          .filter(it -> it.startsWith(prefix))
          .collect(Collectors.toSet());
    }
    String sql =
        String.format("select file_path from %s where file_path like '%s%%'", tableName, prefix);
    return new HashSet<>(template.queryForList(sql, String.class));
  }

  @Override
  public boolean renameTo(Map<String, String> renameKeys) {
    List<Object[]> args =
        renameKeys.entrySet().stream()
            .map(entry -> new Object[] {entry.getValue(), entry.getKey()})
            .collect(Collectors.toList());
    String sql = String.format("update %s set file_path = ? where file_path = ?", tableName);
    int affectedRows = Arrays.stream(template.batchUpdate(sql, args)).sum();
    // oracle 执行更新操作成功会返回-2
    if (affectedRows > 0 || affectedRows == -2) {
      renameKeys.forEach(
          (oldKey, newKey) -> this.cachedContent.put(newKey, this.cachedContent.remove(oldKey)));
      return true;
    }
    return false;
  }

  @Override
  public boolean delete() {
    String sql =
        String.format(
            "delete from %s where file_path = ? or file_path like '%s%%'", tableName, path);
    if (template.update(sql, this.path) > 0) {
      this.cachedContent.entrySet().removeIf(entry -> entry.getKey().startsWith(path));
      return true;
    }
    return false;
  }

  @Override
  public Function<String, Resource> mappedFunction() {
    return it -> new DatabaseResource(template, tableName, it, readonly, this.cachedContent, this);
  }

  @Override
  public String toString() {
    return String.format("db://%s/%s", tableName, Objects.toString(this.path, ""));
  }
}
